RND,全称React Node Desktop,起源于RN在爱奇艺PC端的实现,采用React JS framework + node.JS runtime + native UI engine架构,目标是成为最轻量的JS开发桌面应用的跨平台方案。目前爱奇艺PC客户端的大多数页面都是基于RND开发的。
传统的JS开发native应用的方案都是将native组件注入到JS,JS会按照native的开发模式开发应用,更多的是开发语言从C++换到了JS,开发思想还是native的。React JS带来了全新的开发思路,非常好地隔离了JS层和native层,业务开发基于React JS开发范式而不用受native约束。为了适配自研的lyra引擎以及为业务层提供更方便的开发设施,团队对React JS Framework做了深度的适配,接下来将带着大家深入了解React JS Framework,帮助大家理解这个优雅的view层框架。
撰写本篇文章时RN的最新版本是0.57.8,团队适配的RN版本是0.51.0,它依赖的React版本是16.0.0。本文主要针对0.51.0进行说明,0.51.0与0.57.8的差别不大,基本原理是一样的。React 16除了将备受争议的BSD+Patents协议改为MIT协议之外,还带来了许多新特性,比如:允许在render函数中返回节点数组
提供更好的错误处理机制:componentDidCatch
支持自定义DOM属性
但最关键的一点还是React16是一次重写,在保持API不变的情况下,将核心架构改为了Fiber。在介绍React的Fiber架构之前,首先介绍几个概念:React为每种类型的节点都分配了一个数字编号,具体为:
每一个控件都会对应一个fiber对象节点,所有的fiber节点就构成了virtualdom树,fiber对象的结构和表示的意义如下:
一个完整的virtualdom树的结构如下:
节点树主要分为创建和更新两个过程,每个过程都可以分为以下四个阶段:
其中Index是需要渲染的根控件,runApplication函数会调用ReactNative.render函数,将业务的根节点包裹在AppContainer控件中传递给该函数:
其中RootComponent就是注册的根控件,AppContainer是RN提供的自定义控件。在该函数中会创建一个HostRoot节点,该节点挂载在root对象的current属性上,root对象就是整个节点树的根。接着会调用addTopLevelUpdate主动生成一个update,待更新数据就是传给ReactNative.render的参数,这个过程可以看做是RN内部主动调用了setState函数。然后调用scheduleUpdate函数将待更新的根节点保存在nextScheduledRoot变量中。在RN中更新都是从根节点开始的,无论setState函数在哪个控件上调用。最后调用performWork函数进入workloop阶段。
首先根据HostRoot节点创建待操作节点(注意:RN不是直接处理当前节点,而是处理当前节点的拷贝,也就是节点的alternate属性),然后从该节点开始根据节点类型处理当前节点,也就是beginWork中的各种update方法,并且生成下一个待处理的节点,赋值给nextUnitOfWork。如果nextUnitOfWork不为空,就对其进行处理,否则执行completeUnitOfWork,然后依次遍历处理该节点的兄弟节点和父节点的兄弟节点,直到父节点为空,循环结束。不难发现RN是按照深度优先来创建和更新节点的。具体的创建顺序如下图所示:
节点的update操作包含了对新节点的创建和已有节点更新两种情况,alternate为空是创建。节点的处理主要包括三种:自定义节点、根节点以及基础控件节点。节点的创建和更新都是在响应节点的update函数中。下面主要针对这三种节点进行说明:
HostRoot节点不暴露给业务层,是RN内部使用的。前面说过在创建阶段addTopLevelUpdate函数中会生成一个update,保存在节点的updateQueue属性中,就是通过判断这个属性是否为空来区分是创建节点还是更新节点。如果updateQueue不为空,则取出AppContainer,开始创建AppContainer的fiber节点;如果updateQueue为空(更新阶段)就直接调用bailoutOnAlreadyFinishedWork获取已经创建好的AppContainer的fiber节点返回。ClassComponent是复合控件,也就是通过React.createClass(es5写法)函数创建的控件。控件的创建阶段主要执行三个函数:constructClassInstance:
从fiber节点的type属性中取得控件的构造函数,然后创建一个实例,保存在控件fiber节点的stateNode属性中;
mountClassInstance:
执行实例的componentWillMount函数,如果实例的componentDidMount存在,更新effectTag,待所有子节点处理完毕后再执行;
finishClassComponent:
先执行实例的render函数,然后根据render函数中的返回值执行reconcileChildren函数创建对应的fiber节点。
updateClassInstance:
判断控件是否需要更新shouldUpdate;
根据updateQueue,计算新的state;
存在生命周期函数时标记effectTag
finishClassComponent:
不需要更新时cloneChildFibers;
需要更新时执行instance.render,然后执行reconcileChildren
这个函数中做的工作很少,在创建阶段调用reconcileChildren函数创建子节点的fiber节点并返回,更新阶段调用cloneChildFibers函数复制子节点并返回。在处理节点过程中,如果遇到节点的子节点为空,那么就会调用completeUnitOfWork函数。该函数根据节点类型进行相应处理:
如果是HostComponent,在创建阶段就会创建实例(createInstance),生成_nativeTag,生成createView命令,并且将子节点的HostComponent添加到实例的children属性中,发送setChildren命令添加节点,在更新阶段则标记是否需要更新。如果是ClassComponent节点则无需处理,因为在update阶段已经处理完毕。根据节点的effectTag值,向节点的firstEffect、nextEffect、lastEffect赋值,节点的firstEffect、nextEffect、lastEffect组成一个单链表结构,父节点会继承子节点的相应属性值,这些值会在接下来的commitAllWork阶段被处理。
workloop阶段执行完毕就进入到commitAllWork阶段。该阶段会调用以下两个函数:- commitAllHostEffects。由节点的firstEffect开始遍历,根据effectTag值进行相应的操作,节点更新、插入、删除等。
- commitAllLifeCycles。改变root.current的值;执行生命周期函数:componentDidMount、componentDidUpdate等;执行ref函数。
节点的update函数中已经包含了节点的创建和更新两种情况。这里主要说一下更新的流程和初始化阶段。节点的更新一般是通过实例的setState函数触发的,setState函数会调用Updater.enqueueSetState函数将需要更新的数据保存在fiber节点的updateQueue属性中,然后从当前节点开始向上更新父节点的优先级,更新到根节点结束。然后从根节点开始进入workloop和commitAllWork阶段。RND中JS层与native层的通信与RN是类似的,具体的通信机制如下所示:
JS端的messageQueue模块负责消息的接收和发送,JS端产生的命令会存储在messageQueue中,最后通过调用native向JS注入的接口函数将命令发送到native端,native端的BatchedBridge类负责接收处理JS命令。JS端将messageQueue的实例挂载到global对象上,native就能通过global对象访问到messageQueue中的所有实例属性和方法。native通过EventEmitter类将消息发送到JS端,实现方式为在JS运行环境中获取到messageQueue实例动态执行代码段。类似于浏览器中的window.eval函数的功能。这样就完成了JS和native端的双向通信。
RND在JS层面还进行了一些优化和扩展,主要集中在bundle拆分、css3动画支持、脚手架工具、typescript声明文件扩展等方面。RN的bundle体积很大,在有多个页面实例时尤为突出。因此考虑将RN框架代码单独分离出来,形成公共的base bundle,而将各页面的业务代码打包成各个jobbundle,从而减少了安装包体积和线上更新时的流量消耗。我们在RND中增加了css3动画支持,应用程序可通过在style中指定符合css3动画规范的animation属性,即可实现高性能的动画效果。类似于RN的react-native run-android命令,RND还扩展支持了run-desktop等脚手架命令。最后,我们也为RND提供了ts声明文件,支持开发者使用ts进行开发。未来团队还将陆续为RND增加一些新的组件和API,特别是与桌面开发相关的特性,例如WindowComponent、Shell API、File API等。至此React JS Framework的整个处理流程大致都说完了,本文目的是希望起到一个抛砖引玉的作用。对于框架源码的分析有助于对框架本身有更深的理解,这样才能发现其本身的优点以及缺点,才能让我们在特定的使用场景中去取舍设计方案,去迭代、去优化、去创新。RND在爱奇艺客户端的成功实践表明,RN同样适用于以运营内容为主的、迭代周期密集的互联网桌面应用,JS非常适合UI和业务逻辑的快速开发。随着各大JS引擎性能不断的优化,很多大厂都推出了基于JS语言的轻量级高性能App应用框架,可以预测在不久的将来,以内容运营为主的桌面产品上,JS很快会成为最受开发者欢迎的语言之一。